package flatlandRPG;

import java.util.ArrayList;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.File;
import java.io.InvalidClassException;
import java.lang.reflect.Constructor;
import org.lwjgl.util.vector.Vector2f;

// -------------------------------------------------------------------------
/**
 * This class generates Entities from either a string or a file.
 *
 * @author evant
 * @author tms
 * @version Apr 23, 2011
 */
public class MapLoader
{
    /**
     * Create a new map from a file on disk
     *
     * @param location
     *            the file location.
     * @return true if all Entities loaded correctly, false otherwise.
     * @throws IOException
     *             when there is a problem reading for the file
     */
    public ArrayList<Entity> load(File location)
        throws IOException
    {
        BufferedReader reader = new BufferedReader(new FileReader(location));
        ArrayList<Entity> loadedEntities = new ArrayList<Entity>();
        String line = "";
        String currentEntity = "";

        while ((line = reader.readLine()) != null)
        {
            line = line.trim();

            // Skip the comment lines
            if (line.startsWith("#"))
            {
                continue;
            }

            while (line.endsWith("+"))
            {
                line += reader.readLine();
            }

            // removes helper stuff & whitespace
            line = line.replace(",", " ");
            line = line.replace("+", " ");
            line = line.replaceAll("[()]", "");

            if (line.isEmpty())
            {
                loadedEntities.add(load(currentEntity));
                currentEntity = "";
            }
            else
            {
                currentEntity += line + "\n";
            }
        }
        // Last line is not blank so we need to add the last entity read.
        loadedEntities.add(load(currentEntity));

        return loadedEntities;
    }


    /**
     * Load a single Entity from a string. The Input string should consist of 2
     * or 3 lines: The first line is the name of the class of the Entity. The
     * class must have a constructor that takes only integers or floats, or a
     * single array of integers or floats. The second line is the values for the
     * constructor, separated by whitespace. All values must be either integers
     * or floats. This line is not needed if the constructor takes no arguments.
     * The last line is two numbers that define the x-y location of the Entity.
     *
     * @param string
     *            the input string
     * @return the Entity generated from the string
     * @throws DataFormatException
     *             when the string is invalid in some way
     */
    @SuppressWarnings("rawtypes")
    public Entity load(String string)
        throws DataFormatException
    {
        String[] lines = string.split("\n");
        String className = lines[0];
        try
        {
            // Get the correct class.
            Class<?> c = Class.forName("flatlandRPG." + className);

            // Find the constructor that either has no arguments or
            // where all the arguments are floats, integers, array of
            // floats, or array of integers.
            Constructor[] constructors = c.getConstructors();
            Constructor floatConstructor = null;
            for (Constructor constructor : constructors)
            {
                Class[] types = constructor.getParameterTypes();
                if (validConstructor(types))
                {
                    floatConstructor = constructor;
                    break;
                }
            }

            if (floatConstructor != null)
            {
                int locationLine = 2;
                Entity newObject = null;
                Class[] paramaterTypes = floatConstructor.getParameterTypes();
                int paramaterLength = paramaterTypes.length;

                // Parse the info depending on the constructor type and
                // create a new Entity with the constructor arguments.
                if (paramaterLength != 0)
                {
                    String params[] = lines[1].split(" +");
                    newObject =
                        (Entity)floatConstructor.newInstance(parseArguments(
                            params,
                            paramaterTypes));
                }
                else
                {
                    newObject = (Entity)floatConstructor.newInstance();
                    // If there are no arguments, then the line the defines
                    // the location of the Entity is one earlier.
                    locationLine = 1;
                }

                // Parse and set the location of the Entity
                String[] location = lines[locationLine].split(" +");
                newObject.setLocation(new Vector2f(Float
                    .parseFloat(location[0]), Float.parseFloat(location[1])));

                // Add the entity to the map
                return newObject;
            }
            else
            {
                throw new InvalidClassException(
                    "Class does not have valid constructor");
            }
        }
        // Wraps all possible exceptions for parsing the map data into the
        // DataFormatException
        catch (Exception e)
        {
            throw new DataFormatException(e);
        }
    }


    /**
     * Returns whether or not the constructor signature is valid. A valid
     * signature either takes no arguments, takes only float or integer
     * arguments, or takes an array of either floats or integers.
     */
    @SuppressWarnings("rawtypes")
    private boolean validConstructor(Class[] types)
    {
        if (types.length == 0)
        {
            return true;
        }

        for (Class type : types)
        {
            if (!(type == Float.class || type == Float.TYPE
                || type == Integer.class || type == Integer.TYPE
                || type == Float[].class || type == Integer[].class))
            {
                return false;
            }
        }
        return true;
    }


    /**
     * Convents the string input for arguments into the correct types for the
     * given constructor signature.
     */
    @SuppressWarnings("rawtypes")
    private Object[] parseArguments(String[] array, Class[] types)
    {
        // If there is only one argument, assume
        // that the constructor takes an array of the arguments.
        if (types.length == 1)
        {
            Float[] floatArray = new Float[array.length];
            for (int i = 0; i < array.length; i++)
            {
                floatArray[i] = Float.parseFloat(array[i]);
            }
            return new Object[] { floatArray };
        }
        // Otherwise, parse the arguments as the correct types
        else
        {
            Object[] floatsOrInts = new Object[array.length];

            for (int i = 0; i < array.length; i++)
            {
                if (types[i] == Float.class || types[i] == Float.TYPE)
                {
                    floatsOrInts[i] = Float.parseFloat(array[i]);
                }
                else if (types[i] == Integer.class || types[i] == Integer.TYPE)
                {
                    floatsOrInts[i] = Integer.parseInt(array[i]);
                }
            }

            return floatsOrInts;
        }
    }


    // -------------------------------------------------------------------------
    /**
     * An exception that is thrown when a loaded string is invalid.
     *
     * @author evant
     * @version Apr 23, 2011
     */
    public class DataFormatException
        extends IOException
    {

        // ----------------------------------------------------------
        /**
         * Create a new DataFormatException object.
         *
         * @param cause
         *            the cause of the exception
         */
        public DataFormatException(Throwable cause)
        {
            super("Unable to parse map file", cause);
        }
    }
}
